﻿#region File Description
//-----------------------------------------------------------------------------
// Game.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
#endregion

#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using QuakeMD2;
#endregion

namespace NonPhotoRealistic
{
    /// <summary>
    /// Sample showing how to implement non-photorealistic rendering techniques,
    /// providing a cartoon shader, edge detection, and pencil sketch rendering effect.
    /// </summary>
    public class NonPhotoRealisticGame : Microsoft.Xna.Framework.Game
    {
        #region Fields


        GraphicsDeviceManager graphics;

        SpriteBatch spriteBatch;
        SpriteFont spriteFont;
        Model model;
        Random random = new Random();

        // Effect used to apply the edge detection and pencil sketch postprocessing.
        Effect postprocessEffect;
        Effect cartoonEffect;
        // Overlay texture containing the pencil sketch stroke pattern.
        Texture2D sketchTexture;

        // Randomly offsets the sketch pattern to create a hand-drawn animation effect.
        Vector2 sketchJitter;
        TimeSpan timeToNextJitter;

        // Custom rendertargets.
        RenderTarget2D sceneRenderTarget;
        RenderTarget2D normalDepthRenderTarget;

        private VertexDeclaration mVertPosColor;
        private VertexDeclaration mVertPosColorTex;
        private VertexDeclaration mVertPosNormTex;

        private MD2 md2;

        private double mdblAnimDelay, mdblRunDelay, mdblJumpDelay = 0;
        public enum meAnim
        {
            stand, run, attack, pain1,
            pain2, pain3, jump, flip,
            salute, taunt, wave, point,
            crstand, crwalk, crattack, crpain,
            crdeath, death1, death2, death3
        }

        // Choose what display settings to use.
        NonPhotoRealisticSettings Settings
        {
            get { return NonPhotoRealisticSettings.PresetSettings[settingsIndex]; }
        }

        int settingsIndex = 0;


        // Current and previous input states.
        KeyboardState lastKeyboardState;
        GamePadState lastGamePadState;
        KeyboardState currentKeyboardState;
        GamePadState currentGamePadState;


        #endregion

        #region Initialization


        public NonPhotoRealisticGame()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            graphics.PreferredBackBufferWidth = 853;
            graphics.PreferredBackBufferHeight = 480;

            graphics.MinimumPixelShaderProfile = ShaderProfile.PS_2_0;
        }

        protected override void Initialize()
        {
            // set window, shader, camera projection, grahics
            //init_XNA_app();

            // set vertex format
            mVertPosColor = new VertexDeclaration(graphics.GraphicsDevice,
                                        VertexPositionColor.VertexElements);

            mVertPosColorTex = new VertexDeclaration(graphics.GraphicsDevice,
                                        VertexPositionColorTexture.VertexElements);
            mVertPosNormTex = new VertexDeclaration(graphics.GraphicsDevice,
                                        VertexPositionNormalTexture.VertexElements);

            md2 = new MD2();
            md2.loadModel(graphics.GraphicsDevice, "tris", "Zarlag", Content);
            md2.setAnimSpeed(5.0f); // 5.0f is the default speed
            md2.setAnim((int)meAnim.stand); 

            // all code for this method must be above this line
            base.Initialize();
        }
        /// <summary>
        /// Load your graphics content.
        /// </summary>
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
            spriteFont = Content.Load<SpriteFont>("Arial");
            model = Content.Load<Model>("Ship");
            postprocessEffect = Content.Load<Effect>("PostprocessEffect");
            sketchTexture = Content.Load<Texture2D>("SketchTexture");

            // Change the model to use our custom cartoon shading effect.
            cartoonEffect = Content.Load<Effect>("CartoonEffect");

            ChangeEffectUsedByModel(model, cartoonEffect);

            // Create two custom rendertargets.
            PresentationParameters pp = graphics.GraphicsDevice.PresentationParameters;

            sceneRenderTarget = new RenderTarget2D(graphics.GraphicsDevice,
                pp.BackBufferWidth, pp.BackBufferHeight, 1,
                pp.BackBufferFormat, pp.MultiSampleType, pp.MultiSampleQuality);

            normalDepthRenderTarget = new RenderTarget2D(graphics.GraphicsDevice,
                pp.BackBufferWidth, pp.BackBufferHeight, 1,
                pp.BackBufferFormat, pp.MultiSampleType, pp.MultiSampleQuality);
        }


        /// <summary>
        /// Unload your graphics content.
        /// </summary>
        protected override void UnloadContent()
        {
            if (sceneRenderTarget != null)
            {
                sceneRenderTarget.Dispose();
                sceneRenderTarget = null;
            }
            if (normalDepthRenderTarget != null)
            {
                normalDepthRenderTarget.Dispose();
                normalDepthRenderTarget = null;
            }
        }


        /// <summary>
        /// Alters a model so it will draw using a custom effect, while preserving
        /// whatever textures were set on it as part of the original effects.
        /// </summary>
        static void ChangeEffectUsedByModel(Model model, Effect replacementEffect)
        {
            // Table mapping the original effects to our replacement versions.
            Dictionary<Effect, Effect> effectMapping = new Dictionary<Effect, Effect>();

            foreach (ModelMesh mesh in model.Meshes)
            {
                // Scan over all the effects currently on the mesh.
                foreach (BasicEffect oldEffect in mesh.Effects)
                {
                    // If we haven't already seen this effect...
                    if (!effectMapping.ContainsKey(oldEffect))
                    {
                        // Make a clone of our replacement effect. We can't just use
                        // it directly, because the same effect might need to be
                        // applied several times to different parts of the model using
                        // a different texture each time, so we need a fresh copy each
                        // time we want to set a different texture into it.
                        Effect newEffect = replacementEffect.Clone(
                                                    replacementEffect.GraphicsDevice);

                        // Copy across the texture from the original effect.
                        newEffect.Parameters["Texture"].SetValue(oldEffect.Texture);

                        newEffect.Parameters["TextureEnabled"].SetValue(
                                                            oldEffect.TextureEnabled);

                        effectMapping.Add(oldEffect, newEffect);
                    }
                }

                // Now that we've found all the effects in use on this mesh,
                // update it to use our new replacement versions.
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    meshPart.Effect = effectMapping[meshPart.Effect];
                }
            }
        }


        #endregion

        #region Update and Draw
        
        protected override void Update(GameTime gameTime)
        {
            HandleInput(gameTime);

            // Update the sketch overlay texture jitter animation.
            if (Settings.SketchJitterSpeed > 0)
            {
                timeToNextJitter -= gameTime.ElapsedGameTime;

                if (timeToNextJitter <= TimeSpan.Zero)
                {
                    sketchJitter.X = (float)random.NextDouble();
                    sketchJitter.Y = (float)random.NextDouble();

                    timeToNextJitter += TimeSpan.FromSeconds(Settings.SketchJitterSpeed);
                }
            }

            md2.updateModel(graphics.GraphicsDevice, gameTime);           

            base.Update(gameTime);
        }
                
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice device = graphics.GraphicsDevice;

            // Calculate the camera matrices.
            Viewport viewport = device.Viewport;

            float aspectRatio = (float)viewport.Width / (float)viewport.Height;

            float time = (float)gameTime.TotalGameTime.TotalSeconds;

            Matrix rotation = Matrix.CreateRotationY(time * 0.05f);
            Matrix scale = Matrix.CreateScale(50f);

            Matrix view = Matrix.CreateLookAt(new Vector3(3000, 1500, 0),
                                              Vector3.Zero,
                                              Vector3.Up);

            Matrix projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4,
                                                                    aspectRatio,
                                                                    1000, 10000);
            
            // If we are doing edge detection, first off we need to render the
            // normals and depth of our model into a special rendertarget.
            if (Settings.EnableEdgeDetect)
            {
                device.SetRenderTarget(0, normalDepthRenderTarget);

                device.Clear(Color.Black);

                DrawModel(rotation, view, projection, "NormalDepth");
                DrawMD2Model(scale, view, projection, "NormalDepth");
            }

            // If we are doing edge detection and/or pencil sketch processing, we
            // need to draw the model into a special rendertarget which can then be
            // fed into the postprocessing shader. Otherwise can just draw it
            // directly onto the backbuffer.
            if (Settings.EnableEdgeDetect || Settings.EnableSketch)
                device.SetRenderTarget(0, sceneRenderTarget);
            else
                device.SetRenderTarget(0, null);

            device.Clear(Color.CornflowerBlue);

            // Draw the model, using either the cartoon or lambert shading technique.
            string effectTechniqueName;

            if (Settings.EnableToonShading)
                effectTechniqueName = "Toon";
            else
                effectTechniqueName = "Lambert";

            DrawModel(rotation, view, projection, effectTechniqueName);
            DrawMD2Model(scale, view, projection, effectTechniqueName);
            

            // Run the postprocessing filter over the scene that we just rendered.
            if (Settings.EnableEdgeDetect || Settings.EnableSketch)
            {
                device.SetRenderTarget(0, null);

                ApplyPostprocess();
            }

            // Display some text over the top. Note how we draw this after the
            // postprocessing, because we don't want the text to be affected by it.
            DrawOverlayText();

            base.Draw(gameTime);
        }

        void DrawMD2Model(Matrix world, Matrix view, Matrix projection,
                       string effectTechniqueName)
        {
            // Set suitable renderstates for drawing a 3D model.
            RenderState renderState = graphics.GraphicsDevice.RenderState;
            renderState.AlphaBlendEnable = false;
            renderState.AlphaTestEnable = false;
            renderState.DepthBufferEnable = true;               

           
            // set the vertex declaration
            graphics.GraphicsDevice.VertexDeclaration = mVertPosNormTex;

            cartoonEffect.CurrentTechnique = cartoonEffect.Techniques[effectTechniqueName];
           
            cartoonEffect.Parameters["View"].SetValue(view);
            cartoonEffect.Parameters["Projection"].SetValue(projection);
            cartoonEffect.Parameters["Texture"].SetValue(md2.getTexture());
            cartoonEffect.Parameters["TextureEnabled"].SetValue(true);
            //cartoonEffect.CommitChanges();

            cartoonEffect.Begin(SaveStateMode.None);            
            cartoonEffect.CurrentTechnique.Passes[0].Begin();
            cartoonEffect.Parameters["World"].SetValue(world);
            // set the vertex source to the mesh's vertex buffer
            graphics.GraphicsDevice.Vertices[0].SetSource(
                md2.vertexBuffer, 0, VertexPositionNormalTexture.SizeInBytes);
            graphics.GraphicsDevice.DrawPrimitives(
                    PrimitiveType.TriangleList, 0, md2.getNumTriangles());
            cartoonEffect.CurrentTechnique.Passes[0].End();
            cartoonEffect.End();            
        }
 
        void DrawModel(Matrix world, Matrix view, Matrix projection,
                       string effectTechniqueName)
        {
            // Set suitable renderstates for drawing a 3D model.
            RenderState renderState = graphics.GraphicsDevice.RenderState;

            renderState.AlphaBlendEnable = false;
            renderState.AlphaTestEnable = false;
            renderState.DepthBufferEnable = true;

            // Look up the bone transform matrices.
            Matrix[] transforms = new Matrix[model.Bones.Count];

            model.CopyAbsoluteBoneTransformsTo(transforms);

            // Draw the model.
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (Effect effect in mesh.Effects)
                {
                    // Specify which effect technique to use.
                    effect.CurrentTechnique = effect.Techniques[effectTechniqueName];

                    Matrix localWorld = transforms[mesh.ParentBone.Index] * world;

                    effect.Parameters["World"].SetValue(localWorld);
                    effect.Parameters["View"].SetValue(view);
                    effect.Parameters["Projection"].SetValue(projection);
                }

                mesh.Draw();
            }
        }

        void ApplyPostprocess()
        {
            EffectParameterCollection parameters = postprocessEffect.Parameters;
            string effectTechniqueName;

            // Set effect parameters controlling the pencil sketch effect.
            if (Settings.EnableSketch)
            {
                parameters["SketchThreshold"].SetValue(Settings.SketchThreshold);
                parameters["SketchBrightness"].SetValue(Settings.SketchBrightness);
                parameters["SketchJitter"].SetValue(sketchJitter);
                parameters["SketchTexture"].SetValue(sketchTexture);
            }

            // Set effect parameters controlling the edge detection effect.
            if (Settings.EnableEdgeDetect)
            {
                Vector2 resolution = new Vector2(sceneRenderTarget.Width,
                                                 sceneRenderTarget.Height);

                Texture2D normalDepthTexture = normalDepthRenderTarget.GetTexture();

                parameters["EdgeWidth"].SetValue(Settings.EdgeWidth);
                parameters["EdgeIntensity"].SetValue(Settings.EdgeIntensity);
                parameters["ScreenResolution"].SetValue(resolution);
                parameters["NormalDepthTexture"].SetValue(normalDepthTexture);

                // Choose which effect technique to use.
                if (Settings.EnableSketch)
                {
                    if (Settings.SketchInColor)
                        effectTechniqueName = "EdgeDetectColorSketch";
                    else
                        effectTechniqueName = "EdgeDetectMonoSketch";
                }
                else
                    effectTechniqueName = "EdgeDetect";
            }
            else
            {
                // If edge detection is off, just pick one of the sketch techniques.
                if (Settings.SketchInColor)
                    effectTechniqueName = "ColorSketch";
                else
                    effectTechniqueName = "MonoSketch";
            }

            // Activate the appropriate effect technique.
            postprocessEffect.CurrentTechnique =
                                    postprocessEffect.Techniques[effectTechniqueName];

            // Draw a fullscreen sprite to apply the postprocessing effect.
            spriteBatch.Begin(SpriteBlendMode.None,
                              SpriteSortMode.Immediate,
                              SaveStateMode.None);

            postprocessEffect.Begin();
            postprocessEffect.CurrentTechnique.Passes[0].Begin();

            spriteBatch.Draw(sceneRenderTarget.GetTexture(), Vector2.Zero, Color.White);

            spriteBatch.End();

            postprocessEffect.CurrentTechnique.Passes[0].End();
            postprocessEffect.End();
        }

        void DrawOverlayText()
        {
            string text = "A = settings (" + Settings.Name + ")";

            spriteBatch.Begin();

            // Draw the string twice to create a drop shadow, first colored black
            // and offset one pixel to the bottom right, then again in white at the
            // intended position. This makes text easier to read over the background.
            spriteBatch.DrawString(spriteFont, text, new Vector2(65, 65), Color.Black);
            spriteBatch.DrawString(spriteFont, text, new Vector2(64, 64), Color.White);

            spriteBatch.End();
        }


        #endregion

        #region Handle Input


        /// <summary>
        /// Handles input for quitting or changing the display settings.
        /// </summary>
        void HandleInput(GameTime gameTime)
        {
            lastKeyboardState = currentKeyboardState;
            lastGamePadState = currentGamePadState;

            currentKeyboardState = Keyboard.GetState();
            currentGamePadState = GamePad.GetState(PlayerIndex.One);

            // Check for exit.
            if (currentKeyboardState.IsKeyDown(Keys.Escape) ||
                currentGamePadState.Buttons.Back == ButtonState.Pressed)
            {
                Exit();
            }

            // Switch to the next settings preset?
            if ((currentGamePadState.Buttons.A == ButtonState.Pressed &&
                 lastGamePadState.Buttons.A != ButtonState.Pressed) ||
                (currentKeyboardState.IsKeyDown(Keys.A) &&
                 lastKeyboardState.IsKeyUp(Keys.A)))
            {
                settingsIndex = (settingsIndex + 1) %
                                NonPhotoRealisticSettings.PresetSettings.Length;
            }

            //update animation key
            if (gameTime.TotalGameTime.TotalMilliseconds - mdblAnimDelay > 200)
            {
                mdblAnimDelay = gameTime.TotalGameTime.TotalMilliseconds;
                if (currentKeyboardState.IsKeyDown(Keys.Space) )
                {
                    md2.advanceAnimation();
                }
            }

            if (currentKeyboardState.IsKeyDown(Keys.P) &&
                 lastKeyboardState.IsKeyUp(Keys.P))
            {
                md2.Pause();
            }
            else if (currentKeyboardState.IsKeyDown(Keys.R) &&
                 lastKeyboardState.IsKeyUp(Keys.R))
            {
                md2.Resume();
            }
        }


        #endregion
    }


    #region Entry Point

    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    static class Program
    {
        static void Main()
        {
            using (NonPhotoRealisticGame game = new NonPhotoRealisticGame())
            {
                game.Run();
            }
        }
    }

    #endregion
}
